package obecto.task.progress
{
	import flash.events.ProgressEvent;
	import flash.utils.Dictionary;
	
	import obecto.event.TaskContainerEvent;
	import obecto.event.TaskEvent;
	import obecto.task.BaseTask;
	import obecto.task.ITask;
	import obecto.task.container.ITaskContainer;
	import obecto.utility.DictionaryUtil;
	import obecto.utility.Listener;
	
	[Event(name="start", type="obecto.event.TaskEvent")]
	[Event(name="interrupted", type="obecto.event.TaskEvent")]
	[Event(name="complete", type="obecto.event.TaskEvent")]
	[Event(name="progress", type="flash.events.ProgressEvent")]

	/**
	 * The <code>ProgressMonitor</code> is responsible for organizing and monitoring the 
	 * progress of a complex set of tasks. The <code>ProgressMonitor</code> also provides 
	 * a mechanism to set progress intervals for each task. If a progress interval is not 
	 * set for a task, then the <code>ProgressMonitor</code> calculates what part of the 
	 * progress interval to give for that task, giving an equal parts for each task added 
	 * to the <code>ProgressMonitor</code>.
	 * <p>
	 * With the use of the <code>ProgressMonitor</code> we can easily specify for a task 
	 * to reserve for example 30% of the progress interval, for another task to take 15% 
	 * of the progress interval, and for the rest to share what's left of the interval 
	 * (in the case these are 55%, which divided among 11 task result in 5% from the 
	 * progress interval for each task).
	 */
	 public class ProgressMonitor extends BaseTask implements IProgressiveTask
	 {
		private var lastGeneratedItemID : Number = 0;
	
		private var itemParts : Dictionary;
		private var items : Dictionary;
		private var fixedItems : Dictionary;
		private var completedItems : Dictionary;
		
		private var progressListeners : Dictionary;
		private var completeListeners : Dictionary;
		
		private var variableItemsCount : Number = 0;
		private var fixedItemsCount : Number = 0;
		private var fixedItemsPartSum : Number = 0;
		
		private var _progress : Number = 0;	
		public function get progress() : Number
		{
			return _progress;	
		}
		
		public function ProgressMonitor() 
		{
			itemParts = new Dictionary();
			items = new Dictionary();
			fixedItems = new Dictionary();
			completedItems = new Dictionary();
			progressListeners = new Dictionary();
			completeListeners = new Dictionary();
		}
		
		override protected function startActual() : void
		{
			if (DictionaryUtil.getItemsCount(completedItems) == DictionaryUtil.getItemsCount(items))
			{
				complete();
			}
		}
		
		/**
		 * Adds the specified items to the <code>ProgressMonitor</code>. Each item will get 
		 * an equal part of the progress interval left from the interval not reserved for the 
		 * fixed-part items. The calculation algorith is the following:
		 * <p>
		 * <code>
		 * item_progress_part = (100 - fixed_part_items_parts) / (count(already_added_items) + count(new_items))
		 * </code>
		 * 
		 * @param Array of ITask or IProgressiveTask items
		 */
		public function addItems(items : Array) : void
		{
			for (var i : Number = 0; i < items.length; i++)
			{
				addTask(items[i]);
			}
		}
		
		/**
		 * Adds a single task to the <code>ProgressMonitor</code>. 
		 * 
		 * @param ITask the task to be added
		 */
		public function addTask(task : ITask) : void
		{
			if(task is IProgressiveTask)
			{
				addItem(task as IProgressiveTask);
			}
			else if(task is ITaskContainer)
			{
				monitorTasksContainer(task as ITaskContainer);
			}
			else if(task is ITask)
			{
				var fakeProgressDispatcher : FakeProgressDispatcher
					= new FakeProgressDispatcher(task);	
			
				addItem(fakeProgressDispatcher);	
			}
			else
			{
				throw new Error("Passed argument is not instance of IProgressiveTask or ITask or ITaskContainer"); 
			}
		}
		
		//TODO: removeTask()
		
		/**
		 * Adds a single task to the <code>ProgressMonitor</code>, which will reserve 
		 * a certain part of the progressing interval for itself. 
		 * 
		 * @param ITask the task to be added
		 * @param Number the percentage part reserved from the progress interval, solely 
		 * devoded to the specified task
		 */
		public function addFixedPartTask(task : ITask, percentagePart : Number) : void
		{
			if(task is IProgressiveTask)
			{
				addFixedPartItem(IProgressiveTask(task), percentagePart);
			}
			else if (task is ITaskContainer)
			{
				addFixedPartTaskContainer(ITaskContainer(task), percentagePart);
			}
			else if(task is ITask)
			{
				var fakeProgressDispatcher : FakeProgressDispatcher
					= new FakeProgressDispatcher(task);
					
				addFixedPartItem(fakeProgressDispatcher, percentagePart);	
			}
			else
			{
				throw new Error("Passed argument is not instance of IProgressiveTask or ITask");
			}
		}
		
		private function addFixedPartTaskContainer(container : ITaskContainer, part : Number) : void
		{
			var tasks : Array = container.getAllTasks();
			if (tasks.length > 0)
			{
				for each (var task : ITask in tasks) 
				{
					addFixedPartTask(task, part / tasks.length);
				}
			}
			
			container.addEventListener(TaskContainerEvent.TASK_ADDED, handleMonitoredContainerTaskAdded);
			container.addEventListener(TaskContainerEvent.TASKS_ADDED, handleMonitoredContainerTasksAdded);
			container.addEventListener(TaskContainerEvent.TASK_REMOVED, handleMonitoredContainerTaskRemoved);
			container.addEventListener(TaskContainerEvent.TASKS_REMOVED, handleMonitoredContainerTasksRemoved);
		} 
		
		/**
		 * Adds a prepared IProgressiveTask to the <code>ProgressMonitor</code>.
		 * 
		 * @param IProgressiveTask the item to be added
		 */
		public function addItem(item : IProgressiveTask) : void
		{
			lastGeneratedItemID++;
			variableItemsCount++;
			
			items[lastGeneratedItemID] = item;
			
			recalculateItemParts();
			setItemListeners(item, lastGeneratedItemID);
		}
		
		/**
		 * Adds a prepared IProgressiveTask to the <code>ProgressMonitor</code>, which will reserve 
		 * a certain part of the progressing interval for itself. 
		 * 
		 * @param IProgressiveTask the item to be added
		 * @param Number the percentage part reserved from the progress interval, solely 
		 * devoded to the specified item
		 */
		public function addFixedPartItem(item : IProgressiveTask, percentagePart : Number) : void
		{
			lastGeneratedItemID++;
			fixedItemsCount++;
			
			items[lastGeneratedItemID] = item;
			itemParts[lastGeneratedItemID] = percentagePart;
			
			fixedItems[lastGeneratedItemID] = item;
			fixedItemsPartSum += percentagePart;
			
			recalculateItemParts();
			setItemListeners(item, lastGeneratedItemID);
		}
		
		/**
		 * Removes all items added to the <code>ProgressMonitor</code>.
		 */
		public function removeAllItems() : void
		{
			for (var key : Object in items)
			{
				removeItem(IProgressiveTask(items[key]));
			}
		}
		
		/**
		 * Removes the specified items from the <code>ProgressMonitor</code>.
		 * 
		 * @param Array the items to be removed
		 */
		public function removeItems(items : Array) : void
		{
			for (var i:Number = 0; i < items.length; i++)
			{
				removeItem(IProgressiveTask(items[i]));
			}
		}
		
		/**
		 * Removes the specified item from the <code>ProgressMonitor</code>.
		 * 
		 * @param IProgressiveTask the item to be removed
		 */
		public function removeItem(item : IProgressiveTask) : void
		{
			var itemID : Number;
			
			for (var key : Object in items)
			{
				if(items[key] === item)
				{
					itemID = Number(key);
					break;
				}
			}
			
			if(fixedItems[itemID])
			{
				delete fixedItems[itemID];
				fixedItemsCount--;
				fixedItemsPartSum -= itemParts[itemID];
			}
			else
			{
				variableItemsCount--;
			}
			
			Listener(progressListeners[itemID]).stop();
			Listener(completeListeners[itemID]).stop();
			
			delete progressListeners[itemID];
			delete completeListeners[itemID];
			delete completedItems[itemID];
			delete itemParts[itemID];
			delete items[itemID];
			
			recalculateItemParts();
		}
		
		private function setItemListeners(item:IProgressiveTask, itemID:Number) : void
		{
			progressListeners[itemID] = Listener.createDataForwardingListener(
				item, ProgressEvent.PROGRESS,
				handleItemProgressed,
				{itemID: itemID}
			);
	
			completeListeners[itemID] = Listener.createSingleTimeDataForwardingListener(
				item, TaskEvent.COMPLETE,
				handleItemCompleted,
				{itemID: itemID}
			);
		}
		
		private function recalculateItemParts() : void
		{
			var variableItemPart : Number = (100 - fixedItemsPartSum) / variableItemsCount;
			
			for (var key : Object in items)
			{
				if(!DictionaryUtil.containsKey(key, fixedItems))
				{
					itemParts[key] = variableItemPart;
				}
			}
		}
	
		private function handleItemProgressed(aEvent : Object, aData : Object = null) : void
		{
			if (running)
			{
				var actualProgress : Number = 0;
				
				for (var key:String in items)
				{
					if (DictionaryUtil.containsKey(key, completedItems))
					{
						actualProgress += itemParts[key];
					}
					else
					{
						actualProgress += itemParts[key] * (IProgressiveTask(items[key]).progress / 100);				
					}
				}
				
				if(actualProgress > _progress)
				{
					_progress = actualProgress;
					
					dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS, false, false, _progress, 100));
				}
			}
		}
		
		private function handleItemCompleted(aEvent : Object, aData : Object = null) : void
		{
			var itemID : Number = aData.itemID;
			
			Listener(progressListeners[itemID]).stop();
			completedItems[itemID] = true;
			
			if(running)
			{	
				handleItemProgressed(aEvent);
				
				if(DictionaryUtil.getCount(completedItems) == DictionaryUtil.getCount(items))
				{
					complete();
				}
			}
		}
		
		public function getProgress() : Number 
		{
			return _progress;
		}
		
		public function monitorTasksContainer(container : ITaskContainer) : void
		{
			var containedTasks : Array = container.getAllTasks();
			addItems(containedTasks);
			
			container.addEventListener(TaskContainerEvent.TASK_ADDED, handleMonitoredContainerTaskAdded);
			container.addEventListener(TaskContainerEvent.TASKS_ADDED, handleMonitoredContainerTasksAdded);
			container.addEventListener(TaskContainerEvent.TASK_REMOVED, handleMonitoredContainerTaskRemoved);
			container.addEventListener(TaskContainerEvent.TASKS_REMOVED, handleMonitoredContainerTasksRemoved);
		}
			
		private function handleMonitoredContainerTaskAdded(event : TaskContainerEvent) : void 
		{
			addTask(event.task);
		}
	
		private function handleMonitoredContainerTasksAdded(event : TaskContainerEvent) : void 
		{
			addItems(event.tasks);
		}
	
		private function handleMonitoredContainerTaskRemoved(event : TaskContainerEvent) : void 
		{
			removeItem(IProgressiveTask(event.task));
		}
	
		private function handleMonitoredContainerTasksRemoved(event : TaskContainerEvent) : void 
		{
			removeItems(event.tasks);
		}
	}
}